import RtcEngine, {
  RtcChannel,
  ClientRole,
  AudioScenario,
  AudioProfile,
  ConnectionStateType,
  ConnectionChangedReason,
  NetworkType,
  CameraCaptureOutputPreference,
  CameraDirection,
  FacePositionInfo,
  AudioLocalState,
  AudioLocalError,
  AudioVolumeInfo,
  LocalVideoStreamState,
  LocalVideoStreamError
} from 'react-native-agora';
import { includes } from 'ramda';

import { EngineProp, JoinChannelProp, LeaveChannelProp } from './types';
import { DEVICE_MODEL, isIOS } from '../../constant';
import Log from '../../common/log';
import { CRASH_MODELS } from './constant';
import tracker from '../actionTracker';
import {
  setClassRoomMicrophone,
  setClassRoomMicrophoneRes
} from '../../constant/events';
import agoraRawData from '../native/agoraRawData';

const AGORA_APP_ID = 'b3422e5e261645bba07c936d49c5dd68';
const TAG = 'AgoraLog';
const MAX_TRY_AVAILABLE_AUDIO_NUMBER = 1;

let engine: RtcEngine | null;
let BroadcasterRtcChannel: RtcChannel | null;
let engineProp: EngineProp = {
  uid: 0,
  role: ClientRole.Audience
};
let streamId = 0;
let audioLocalState: AudioLocalState;
let tryAvailableAudioNumber = 0;
let tryAvailableVideoNumber = 0;
let joinChannelTimer: NodeJS.Timer | null;

const clearJoinChannelTimer = (): void => {
  if (joinChannelTimer) {
    Log.i(
      TAG,
      JSON.stringify({
        event: 'clearJoinChannelTimer'
      })
    );
    clearTimeout(joinChannelTimer);
    joinChannelTimer = null;
  }
};

export const initEngine = async (props: EngineProp): Promise<void> => {
  engineProp = { ...props };

  const { role } = engineProp;

  Log.i(
    TAG,
    JSON.stringify({
      event: 'initEngine start'
    })
  );

  engine = await RtcEngine.create(AGORA_APP_ID);

  Log.i(
    TAG,
    JSON.stringify({
      event: 'initEngine end'
    })
  );

  engine.enableWebSdkInteroperability(true);
  engine.enableAudioVolumeIndication(1000, 3, true);

  engine
    .setVideoEncoderConfiguration({
      bitrate: 50,
      dimensions: { width: 80, height: 80 }
    })
    .then((res): void => {
      Log.i(
        TAG,
        JSON.stringify({
          event: 'setVideoEncoderConfiguration',
          res
        })
      );
    });
  engine.setCameraCapturerConfiguration({
    cameraDirection: CameraDirection.Front,
    preference: CameraCaptureOutputPreference.Performance
  });

  if (isIOS) {
    engine.enableFaceDetection(true);
  }
  engine.enableAudio();
  engine.enableVideo();

  if (engine) {
    const handle = await engine.getNativeHandle();
    agoraRawData.registerVideoFrameObserver(handle);
  }

  if (role === ClientRole.Audience) {
    engine.enableLocalVideo(false);
    engine.enableLocalAudio(false);
  }
  engine.setClientRole(role);
  engine.createDataStream(false, false).then((res): void => {
    streamId = res;
  });
  engine
    .setAudioProfile(
      AudioProfile.SpeechStandard,
      includes(DEVICE_MODEL)(CRASH_MODELS)
        ? AudioScenario.GameStreaming
        : AudioScenario.GameStreaming
    )
    .then((res): void => {
      Log.i(
        TAG,
        JSON.stringify({
          event: 'setAudioProfile',
          res
        })
      );
    });
  engine.registerMediaMetadataObserver();

  // 本地网络类型发生改变回调
  engine.addListener('NetworkTypeChanged', (type: NetworkType): void => {
    Log.i(
      TAG,
      JSON.stringify({
        event: 'NetworkTypeChanged',
        type
      })
    );
  });

  // 本地音频状态发生改变回调
  engine.addListener(
    'LocalAudioStateChanged',
    (state: AudioLocalState, error: AudioLocalError): void => {
      audioLocalState = state;
      Log.i(
        TAG,
        JSON.stringify({
          event: 'LocalAudioStateChanged',
          state,
          error
        })
      );
    }
  );

  // 本地视频状态发生改变回调
  engine.addListener(
    'LocalVideoStateChanged',
    (state: LocalVideoStreamState, error: LocalVideoStreamError): void => {
      Log.i(
        TAG,
        JSON.stringify({
          event: 'LocalVideoStateChanged',
          state,
          error
        })
      );
    }
  );

  engine.setParameters('{"che.audio.keep.audiosession": true}');
  engine.setParameters('{"che.audio.mixable.option":true}');
};

export const setVideoAvailable = (available: boolean): void => {
  if (engine) {
    const { role } = engineProp;
    const enable = !!(available && role === ClientRole.Broadcaster);
    Log.i(
      TAG,
      JSON.stringify({
        event: 'setVideoAvailable',
        available
      })
    );
    engine
      .enableLocalVideo(enable)
      .then((res): void => {
        tryAvailableVideoNumber = 0;
        Log.i(
          TAG,
          JSON.stringify({
            event: 'setVideoAvailableRes',
            tryAvailableVideoNumber,
            isOpen: available,
            isSuccess: true
          })
        );
      })
      .catch((error): void => {
        if (tryAvailableVideoNumber < MAX_TRY_AVAILABLE_AUDIO_NUMBER) {
          tryAvailableVideoNumber += 1;
          setVideoAvailable(available);
        }
        Log.i(
          TAG,
          JSON.stringify({
            event: 'setVideoAvailableRes',
            tryAvailableVideoNumber,
            isOpen: available,
            isSuccess: false
          })
        );
      });
  }
};

export const joinChannel = (props: JoinChannelProp): Promise<RtcChannel> =>
  new Promise(
    async (resolve, reject): Promise<void> => {
      const {
        token,
        channel,
        onJoin,
        onUserJoin,
        onUserLeave,
        onMessage,
        onReJoin,
        getToken,
        userRole = ClientRole.Broadcaster,
        onFacePositionChange,
        audioVolumeIndication
      } = props;
      const { uid } = engineProp;

      Log.i(
        TAG,
        JSON.stringify({
          event: 'joinChannel start',
          channel,
          token
        })
      );

      const rtcChannel = await RtcChannel.create(channel);
      if (!rtcChannel) {
        Log.i(
          TAG,
          JSON.stringify({
            event: 'joinChannel fail',
            channel,
            token
          })
        );
        reject(new Error('join channel fail'));
        return;
      }

      setVideoAvailable(true);

      BroadcasterRtcChannel = rtcChannel;

      const setClientRoleAndPublish = (): void => {
        rtcChannel
          .setClientRole(userRole)
          .then((): void => {
            Log.i(
              TAG,
              JSON.stringify({
                event: 'setClientRole success',
                channel
              })
            );
            rtcChannel
              .publish()
              .then((): void => {
                Log.i(
                  TAG,
                  JSON.stringify({
                    event: 'publish success',
                    channel
                  })
                );
                resolve(rtcChannel);
              })
              .catch((error): void => {
                Log.i(
                  TAG,
                  JSON.stringify({
                    event: 'publish fail',
                    channel,
                    error
                  })
                );
                reject(new Error(`publish fail : ${error}`));
              });
          })
          .catch((error): void => {
            Log.i(
              TAG,
              JSON.stringify({
                event: 'setClientRole fail',
                channel,
                error
              })
            );
            reject(new Error(`setClientRole fail : ${error}`));
          });
      };

      const renewToken = (needRejoin = false): void => {
        if (getToken) {
          getToken().then((newToken: string): void => {
            if (needRejoin) {
              rtcChannel
                .joinChannel(newToken, '用户进入频道', uid, {
                  autoSubscribeAudio: true,
                  autoSubscribeVideo: true
                })
                .then((): void => {
                  Log.i(
                    TAG,
                    JSON.stringify({
                      event: 'joinChannel success',
                      uid
                    })
                  );
                  if (onJoin) onJoin(rtcChannel);
                  setClientRoleAndPublish();
                });
            } else {
              rtcChannel.renewToken(newToken).then((): void => {
                Log.i(
                  TAG,
                  JSON.stringify({
                    event: 'renewToken',
                    token
                  })
                );
              });
            }
          });
        }
      };

      // Token 服务即将过期回调
      rtcChannel.addListener(
        'TokenPrivilegeWillExpire',
        (newToken: string): void => {
          Log.i(
            TAG,
            JSON.stringify({
              event: 'TokenPrivilegeWillExpire',
              token: newToken
            })
          );
          renewToken();
        }
      );

      // 网络连接状态已改变回调
      rtcChannel.addListener(
        'ConnectionStateChanged',
        (state: ConnectionStateType, reason: ConnectionChangedReason): void => {
          Log.i(
            TAG,
            JSON.stringify({
              event: 'ConnectionStateChanged',
              state,
              reason
            })
          );
          if (reason === ConnectionChangedReason.TokenExpired) {
            renewToken();
          }
        }
      );

      // Token过期回调
      rtcChannel.addListener('RequestToken', (): void => {
        Log.i(
          TAG,
          JSON.stringify({
            event: 'RequestToken'
          })
        );
        renewToken(true);
      });

      // 远端用户加入当前频道回调
      rtcChannel.addListener('UserJoined', (id: number): void => {
        Log.i(
          TAG,
          JSON.stringify({
            event: 'UserJoined',
            uid: id
          })
        );
        if (onUserJoin) onUserJoin(id);
      });

      // 远端用户离开当前频道回调
      rtcChannel.addListener('UserOffline', (id: number): void => {
        Log.i(
          TAG,
          JSON.stringify({
            event: 'UserOffline',
            uid: id
          })
        );
        if (onUserLeave) onUserLeave(id);
      });

      // 重新加入频道回调
      rtcChannel.addListener(
        'RejoinChannelSuccess',
        (currentChannel: string, userId: number, elapsed: number): void => {
          Log.i(
            TAG,
            JSON.stringify({
              event: 'RejoinChannelSuccess',
              channel: currentChannel,
              elapsed,
              uid: userId
            })
          );
          if (onReJoin) onReJoin(userId);
        }
      );

      // 接受消息回调
      rtcChannel.addListener(
        'StreamMessage',
        (msgUserId: number, msgStreamId: number, msg: string): void => {
          Log.i(
            TAG,
            JSON.stringify({
              event: 'StreamMessage',
              msgUserId,
              msgStreamId,
              msg
            })
          );
          if (onMessage) onMessage(msg);
        }
      );

      // 加入频道回调
      rtcChannel.addListener(
        'JoinChannelSuccess',
        (currentChannel: string, userId: number, elapsed: number): void => {
          clearJoinChannelTimer();
          Log.i(
            TAG,
            JSON.stringify({
              event: 'JoinChannelSuccess',
              channel: currentChannel,
              elapsed,
              uid: userId
            })
          );
        }
      );

      // 离开频道回调
      rtcChannel.addListener('LeaveChannel', (stats: any): void => {
        Log.i(
          TAG,
          JSON.stringify({
            event: 'LeaveChannel',
            stats
          })
        );
      });

      // 用户角色已切换回调
      rtcChannel.addListener(
        'ClientRoleChanged',
        (oldRole: ClientRole, newRole: ClientRole): void => {
          Log.i(
            TAG,
            JSON.stringify({
              event: 'ClientRoleChanged',
              oldRole,
              newRole
            })
          );
        }
      );

      // 网络连接丢失回调
      rtcChannel.addListener('ConnectionLost', (): void => {
        Log.i(
          TAG,
          JSON.stringify({
            event: 'ConnectionLost'
          })
        );
      });

      // AudioVolumeIndication
      if (engine) {
        engine.addListener(
          'AudioVolumeIndication',
          (speakers: AudioVolumeInfo[], totalVolume: number): void => {
            if (audioVolumeIndication) {
              audioVolumeIndication(speakers, totalVolume);
            }
          }
        );
      }

      // 人脸识别反馈
      if (engine) {
        engine.addListener(
          'FacePositionChanged',
          (
            imageWidth: number,
            imageHeight: number,
            faces: FacePositionInfo[]
          ): void => {
            if (onFacePositionChange) {
              onFacePositionChange(imageWidth, imageHeight, faces);
            }
          }
        );
      }

      const joinChannelAction = (): Promise<void> =>
        rtcChannel
          .joinChannel(token, '用户进入频道', uid, {
            autoSubscribeVideo: true,
            autoSubscribeAudio: true
          })
          .then((res): void => {
            Log.i(
              TAG,
              JSON.stringify({
                event: 'joinChannel join',
                token,
                channel,
                uid
              })
            );
            if (onJoin) onJoin(rtcChannel);

            setClientRoleAndPublish();
          });

      // 加入频道失败的计时器
      if (!joinChannelTimer) {
        Log.i(
          TAG,
          JSON.stringify({
            event: 'setJoinChannelTimer'
          })
        );
        joinChannelTimer = setTimeout((): void => {
          Log.i(
            TAG,
            JSON.stringify({
              event: 'joinChannelFail',
              token,
              channel,
              uid
            })
          );
          joinChannelAction();
        }, 10 * 1000);
      }

      joinChannelAction();
    }
  );

export const leaveChannel = (props: LeaveChannelProp): void => {
  const { onLeave } = props;

  if (engine) {
    clearJoinChannelTimer();
    engine.leaveChannel().then((): void => {
      if (onLeave) onLeave();
      if (engine) engine.removeAllListeners();
    });
  }
};

export const muteAllRemoteAudioStreams = (isMute: boolean): void => {
  if (BroadcasterRtcChannel) {
    Log.i(
      TAG,
      JSON.stringify({
        event: 'muteAllRemoteAudioStreams',
        isMute
      })
    );
    BroadcasterRtcChannel.muteAllRemoteAudioStreams(isMute);
  }
};

export const setAudioAvailable = (available: boolean, reason = ''): void => {
  if (engine) {
    Log.i(
      TAG,
      JSON.stringify({
        event: 'setAudioAvailable',
        audioLocalState,
        available
      })
    );

    tracker.reportAction(setClassRoomMicrophone, {
      reason: JSON.stringify({ isOpen: available, reason, audioLocalState })
    });

    const { role } = engineProp;
    const enable = !!(available && role === ClientRole.Broadcaster);

    if (enable) {
      muteAllRemoteAudioStreams(false);
    } else {
      muteAllRemoteAudioStreams(true);
    }

    engine
      .enableLocalAudio(enable)
      .then((res): void => {
        tryAvailableAudioNumber = 0;
        tracker.reportAction(setClassRoomMicrophoneRes, {
          reason: JSON.stringify({
            isOpen: available,
            reason,
            audioLocalState,
            isSuccess: true
          })
        });
      })
      .catch((error): void => {
        if (tryAvailableAudioNumber < MAX_TRY_AVAILABLE_AUDIO_NUMBER) {
          tryAvailableAudioNumber += 1;
          setAudioAvailable(available, reason);
        }
        tracker.reportAction(setClassRoomMicrophoneRes, {
          reason: JSON.stringify({
            tryAvailableAudioNumber,
            isOpen: available,
            reason,
            audioLocalState,
            isSuccess: false,
            error
          })
        });
      });
  }
};

export const muteRemoteAudioStream = (uid: number, isMute: boolean): void => {
  if (BroadcasterRtcChannel) {
    Log.i(
      TAG,
      JSON.stringify({
        event: 'muteRemoteAudioStream',
        uid,
        isMute
      })
    );
    BroadcasterRtcChannel.muteRemoteAudioStream(uid, isMute);
  }
};

export const muteMultiRemoteAudioStreams = (
  uidList: number[],
  isMute: boolean
): void => {
  uidList.forEach((uid): void => {
    muteRemoteAudioStream(uid, isMute);
  });
};

export const sendStreamMessage = (str: string): void => {
  if (engine && streamId) {
    engine.sendStreamMessage(streamId, str);
  }
};

export const getEngine = (): RtcEngine | null => engine;

export const destroyEngine = (): void => {
  if (engine) {
    agoraRawData.unregisterVideoFrameObserver();

    engine.destroy().then((): void => {
      Log.i(
        TAG,
        JSON.stringify({
          event: 'Destroy Engine'
        })
      );
    });
  }
};

export const setClientRole = (role: ClientRole): void => {
  if (engine) {
    engine.setClientRole(role).then((): void => {
      Log.i(
        TAG,
        JSON.stringify({
          event: 'Set Client Role'
        })
      );
    });
  }
};

export const setVideoEncoderConfiguration = (smallBitRate: boolean): void => {
  if (engine) {
    engine
      .setVideoEncoderConfiguration({
        bitrate: smallBitRate ? 50 : 100,
        dimensions: smallBitRate
          ? { width: 80, height: 80 }
          : { width: 360, height: 360 }
      })
      .then((res): void => {
        Log.i(
          TAG,
          JSON.stringify({
            event: 'setVideoEncoderConfiguration',
            res
          })
        );
      });
  }
};
